﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace HerbertDotNet
{
    [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
    public class HerbertProgram : ICloneable
    {
        protected bool _isCompiled = false;                // True if code has been compiled.
        protected Dictionary<int, HProcedure> _procedures; // Dictionary of information about compiled
        // procedures and their names.

        public static HerbertProgram Parse(string code)
        {
            HParseInfo parseInfo;

            return Parse(code, out parseInfo);
        }

        public static HerbertProgram Parse(string code, out HParseInfo parseInfo)
        {
            List<HInstruction> progData = new List<HInstruction>();
            List<int> charIndices = new List<int>();

            int charIndex = 0;
            char curChar = '\n';
            char lastChar;
            int bracketDepth = 0;
            bool insideProcedure = false;
            string curNumberText = string.Empty;
            List<int> defProcs = new List<int>();

            // Insure that parser does not mistake finding procedure at end of code.
            code += "\n:";

            try
            {
                // Get each char in text.
                for (charIndex = 0; charIndex < code.Length; charIndex++)
                {
                    // Set last char then get current char in code.
                    lastChar = curChar;
                    curChar = code[charIndex];

                    // Check if current number has ended.
                    if (curNumberText.Length > 0 && !char.IsNumber(curChar))
                    {
                        // Add instruction for current number.
                        charIndices.Add(charIndex);
                        progData.Add(HInstruction.NumberBase | (HInstruction)(Convert.ToInt16(curNumberText)));

                        // Clear variables for current number.
                        curNumberText = string.Empty;
                    }

                    switch (curChar)
                    {
                        case 's':
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.Straight);

                            break;
                        case 'l':
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.Left);

                            break;
                        case 'r':
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.Right);

                            break;
                        case '+':
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.Add);

                            break;
                        case '-':
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.Subtract);

                            break;
                        case ':':
                            break;
                        case '(':
                            bracketDepth++;

                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.BeginArgument);

                            break;
                        case ')':
                            bracketDepth--;

                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.EndArgument);

                            break;
                        case ',':
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.EndArgument);
                            charIndices.Add(charIndex);
                            progData.Add(HInstruction.BeginArgument);

                            break;
                        case '\n':
                            // Check if one or more brackets have not been closed.
                            if (bracketDepth > 0) throw new HerbertParserException(
                                "Expected close brackets sign (')').");

                            // Check if inside procedure.
                            if (insideProcedure)
                            {
                                insideProcedure = false;

                                charIndices.Add(charIndex);
                                progData.Add(HInstruction.EndProcedure);
                            }

                            break;
                        default:
                            // Check if current char is number.
                            if (char.IsNumber(curChar))
                            {
                                if (bracketDepth == 0) throw new HerbertParserException(
                                    "Numbers are not allowed at this point in the code.");

                                // Append char to text of current number.
                                curNumberText += curChar;

                                break;
                            }

                            // Check if current char is procedure name.
                            if (char.IsLower(curChar))
                            {
                                int procIndex = HerbertHelper.ProcedureNames.IndexOf(curChar);

                                // Check if this is beginning of procedure definition.
                                if (!insideProcedure && lastChar == '\n' && code.IndexOf(':', charIndex)
                                    < code.IndexOf('\n', charIndex))
                                {
                                    insideProcedure = true;

                                    charIndices.Add(charIndex);
                                    progData.Add(HInstruction.BeginProcedure);

                                    // Add procedure index to list of defined procedures.
                                    defProcs.Add(procIndex);
                                }

                                // Add instruction for procedure name.
                                charIndices.Add(charIndex);
                                progData.Add(HInstruction.ProcedureNameBase | (HInstruction)procIndex);

                                break;
                            }

                            // Check if current char is parameter name.
                            if (char.IsUpper(curChar))
                            {
                                // Add instruction for parameter name.
                                charIndices.Add(charIndex);
                                progData.Add(HInstruction.ParameterNameBase
                                    | (HInstruction)HerbertHelper.ParameterNames.IndexOf(curChar));

                                break;
                            }

                            // Allow certain characters that have no effect on code.
                            if (curChar == '\r') break;
                            if (curChar == ' ') break;

                            throw new HerbertParserException(string.Format(
                                "Invalid character in code ('{0}').", charIndex, curChar));
                    }
                }

                // Check if one or more brackets still have not been closed at end of program.
                if (bracketDepth > 0) throw new HerbertParserException(
                    "Expected close brackets sign (')').");

                // Write EndProcedure if still inside procedure.
                if (insideProcedure)
                {
                    charIndices.Add(code.Length - 1);
                    progData.Add(HInstruction.EndProcedure);
                }
            }
            catch (Exception ex)
            {
                var exParser = ex as HerbertParserException;

                throw new HerbertParserException(string.Format("Error parsing program code at char {0}."
                    + (exParser == null ? string.Empty : "\r\n" + exParser.Message),
                    charIndex), exParser == null ? ex : null);
            }

            // Create array of char indices for parse information.
            parseInfo.InstructionCharIndices = charIndices.ToArray();

            return new HerbertProgram(progData.ToArray());
        }

        public HerbertProgram(HInstruction[] data)
        {
            this.Code = data;
        }

        public HerbertProgram(int size)
        {
            this.Code = new HInstruction[size];
        }

        public HerbertProgram()
        {
        }

        public Dictionary<int, HProcedure> Procedures
        {
            get
            {
                return _procedures;
            }
        }

        public int StartAddress
        {
            get;
            protected set;
        }

        public bool IsCompiled
        {
            get
            {
                return _isCompiled;
            }
        }

        public int Length
        {
            get
            {
                return this.Code.Length;
            }
        }

        public HInstruction[] Code
        {
            get;
            set;
        }

        public void ResetCompiledFlag()
        {
            // Set that program has not been compiled.
            _isCompiled = false;
        }

        protected int GetProcedureBeginEndDiff()
        {
            int count = 0;

            for (int address = 0; address < this.Code.Length; address++)
            {
                if (this.Code[address] == HInstruction.BeginProcedure) count++;
                if (this.Code[address] == HInstruction.EndProcedure) count--;
            }

            return count;
        }

        public void Compile()
        {
            // Create list of procedures.
            this.StartAddress = -1;
            _procedures = new Dictionary<int, HProcedure>();

            // Get each instruction in program.
            HInstruction curInstruction = HInstruction.NoOp;
            HInstruction lastInstruction;
            bool insideProcedure = false;
            bool insideProcedureDefinition = false;
            int curProcedureName = -1;
            int curProcedureBodyAddress = int.MaxValue;
            List<int> curProcedureArgs = null;

            try
            {
                for (int address = 0; address < this.Code.Length; address++)
                {
                    // Set last instruction then get current instruction.
                    lastInstruction = curInstruction;
                    curInstruction = this.Code[address];

                    // Check current instruction.
                    switch (curInstruction)
                    {
                        case HInstruction.BeginProcedure:
                            insideProcedure = true;
                            insideProcedureDefinition = true;
                            curProcedureBodyAddress = address + 1;

                            // Create new list of arguments for current procedure.
                            curProcedureArgs = new List<int>();

                            break;
                        case HInstruction.EndProcedure:
                            insideProcedure = false;

                            try
                            {
                                // Add procedure to dictionary.
                                _procedures.Add(curProcedureName, new HProcedure(curProcedureName,
                                    curProcedureBodyAddress, curProcedureArgs));
                            }
                            catch (ArgumentException)
                            {
                                throw new HerbertCompilerException(string.Format("Cannot define more than one"
                                    + " procedure with the same name ('{0}').", HerbertHelper.ProcedureNames[
                                    curProcedureName]));
                            }

                            break;
                        default:
                            // Check if last instruction could mark end of procedure definition.
                            if (lastInstruction == HInstruction.EndArgument
                                || ((int)(lastInstruction & HInstruction.ProcedureNameBase) != 0))
                            {
                                // Check if current position is inside procedure definition.
                                if (insideProcedureDefinition)
                                {
                                    // Check if current instruction is not BeginArgument.
                                    if (curInstruction != HInstruction.BeginArgument)
                                    {
                                        // Set that current position is outside procedure definition.
                                        insideProcedureDefinition = false;

                                        // Store address at start of procedure body.
                                        curProcedureBodyAddress = address;
                                    }
                                }
                            }

                            // Check if instruction is procedure name.
                            if ((curInstruction & HInstruction.ProcedureNameBase) != 0)
                            {
                                // Check if current position is inside procedure definition.
                                if (insideProcedureDefinition)
                                {
                                    // Store name of procedure.
                                    curProcedureName = (int)(curInstruction
                                        ^ HInstruction.ProcedureNameBase);

                                    break;
                                }
                            }

                            // Check if instruction is parameter name.
                            if ((curInstruction & HInstruction.ParameterNameBase) != 0)
                            {
                                // Check if current position is inside procedure definition.
                                if (insideProcedureDefinition)
                                {
                                    // Add name of argument to list of arguments.
                                    curProcedureArgs.Add((int)(curInstruction
                                        ^ HInstruction.ParameterNameBase));

                                    break;
                                }
                            }

                            break;
                    }

                    // Check if current instruction is first of main program.
                    if (curInstruction != HInstruction.EndProcedure && !insideProcedure
                        && this.StartAddress == -1)
                    {
                        this.StartAddress = address;
                    }
                }

                if (this.StartAddress == -1) this.StartAddress = 0;

                // Set that program has been compiled.
                _isCompiled = true;
            }
            catch (Exception ex)
            {
                var exCompiler = ex as HerbertCompilerException;

                throw new HerbertCompilerException(string.Format("Error compiling program code."
                    + (exCompiler == null ? string.Empty : "\r\n" + exCompiler.Message)),
                    exCompiler == null ? ex : null);
            }
        }

        public int GetByteLength()
        {
            int length = 0;

            // Get each instruction in program.
            HInstruction curInstruction = HInstruction.NoOp;
            HInstruction lastInstruction;
            //bool insideProcedureDefinition = false;

            for (int i = 0; i < this.Code.Length; i++)
            {
                // Set last instruction then get current instruction.
                lastInstruction = curInstruction;
                curInstruction = this.Code[i];

                // Check if current instruction counts as one byte.
                switch (curInstruction)
                {
                    case HInstruction.NoOp:
                        continue;
                    case HInstruction.Add:
                        continue;
                    case HInstruction.Subtract:
                        continue;
                    case HInstruction.BeginProcedure:
                        continue;
                    case HInstruction.EndProcedure:
                        continue;
                    case HInstruction.BeginArgument:
                        continue;
                    case HInstruction.EndArgument:
                        continue;
                    default:
                        break;
                }

                // Increment byte length.
                length++;
            }

            return length;
        }

        public object Clone()
        {
            HerbertProgram obj = new HerbertProgram();

            obj.Code = (HInstruction[])this.Code.Clone();

            return obj;
        }

        public override string ToString()
        {
            StringBuilder code = new StringBuilder();

            // Get each instruction in program.
            HInstruction curInstruction = HInstruction.NoOp;
            HInstruction lastInstruction;
            bool insideProcedureDefinition = false;

            for (int i = 0; i <= this.Code.Length; i++)
            {
                // Set last instruction then get current instruction.
                lastInstruction = curInstruction;
                curInstruction = (i < this.Code.Length) ? this.Code[i] : HInstruction.NoOp;

                if (curInstruction == HInstruction.NoOp && i > this.Code.Length)
                    break;

                // Check if last instruction could mark end of procedure definition.
                if (lastInstruction == HInstruction.EndArgument
                    || ((int)(lastInstruction & HInstruction.ProcedureNameBase) != 0))
                {
                    // Check if current instruction is BeginArgument.
                    if (curInstruction == HInstruction.BeginArgument)
                    {
                        // Check if last instruction was EndArgument.
                        if (lastInstruction == HInstruction.EndArgument)
                            code.Append(',');
                    }
                    else
                    {
                        // Check if last instruction was EndArgument.
                        if (lastInstruction == HInstruction.EndArgument)
                            code.Append(')');

                        // Check if inside definition of procedure.
                        if (insideProcedureDefinition)
                        {
                            // Set that current position is outside procedure definition.
                            insideProcedureDefinition = false;

                            code.Append(':');
                        }
                    }
                }

                // Check if last instruction was EndArgument.
                if ((lastInstruction & HInstruction.ProcedureNameBase) != 0)
                {
                    // Check if current instruction is BeginArgument.
                    if (curInstruction == HInstruction.BeginArgument)
                        code.Append('(');
                }

                // Check current instruction.
                switch (curInstruction)
                {
                    case HInstruction.Straight:
                        code.Append('s');

                        break;
                    case HInstruction.Left:
                        code.Append('l');

                        break;
                    case HInstruction.Right:
                        code.Append('r');

                        break;
                    case HInstruction.Add:
                        code.Append('+');

                        break;
                    case HInstruction.Subtract:
                        code.Append('-');

                        break;
                    case HInstruction.BeginProcedure:
                        if (i > 0 && code[code.Length - 1] != '\n') code.Append("\r\n");

                        // Set that current position is inside procedure definition.
                        insideProcedureDefinition = true;

                        break;
                    case HInstruction.EndProcedure:
                        code.Append("\r\n");

                        // Set that current position is outside procedure definition.
                        insideProcedureDefinition = false;

                        break;
                    case HInstruction.BeginArgument:
                        break;
                    case HInstruction.EndArgument:
                        break;
                    default:
                        // Check if instruction is number.
                        if ((curInstruction & HInstruction.NumberBase) != 0)
                        {
                            // Append number to code.
                            code.Append((byte)(curInstruction ^ HInstruction.NumberBase));

                            break;
                        }

                        // Check if instruction is procedure name.
                        if ((curInstruction & HInstruction.ProcedureNameBase) != 0)
                        {
                            // Append procedure name to code.
                            code.Append(HerbertHelper.ProcedureNames[(int)(curInstruction
                                ^ HInstruction.ProcedureNameBase)]);

                            break;
                        }

                        // Check if instruction is parameter name.
                        if ((curInstruction & HInstruction.ParameterNameBase) != 0)
                        {
                            // Append parameter name to code.
                            code.Append(HerbertHelper.ParameterNames[(int)(curInstruction
                                ^ HInstruction.ParameterNameBase)]);

                            break;
                        }

                        break;
                }
            }

            // Check if last instruction was EndArgument.
            if (curInstruction == HInstruction.EndArgument)
            {
                code.Append(')');
            }

            if (code[code.Length - 2] == '\r' && code[code.Length - 1] == '\n')
                code.Remove(code.Length - 2, 2);

            return code.ToString();
        }
    }

    public struct HParseInfo
    {
        public int[] InstructionCharIndices; // Indices of characters in parsed code for each instruction
        // in program.

        public HParseInfo(int[] instructionCharIndices)
        {
            this.InstructionCharIndices = instructionCharIndices;
        }
    }

    [System.Diagnostics.DebuggerDisplay("{ToString(),nq}")]
    public struct HProcedure
    {
        public int Name;
        public int BodyAddress;
        public List<int> ArgumentNames;

        public HProcedure(int name, int bodyAddress, List<int> argumentNames)
        {
            this.Name = name;
            this.BodyAddress = bodyAddress;
            this.ArgumentNames = argumentNames;
        }

        public override string ToString()
        {
            return HerbertHelper.ProcedureNames[Name].ToString();
        }
    }

    [System.Diagnostics.DebuggerDisplay("{HerbertHelper.GetInstructionText(this),nq}")]
    public enum HInstruction : ushort
    {
        NoOp = 0x0000,
        Straight = 0x0001,
        Left = 0x0002,
        Right = 0x0003,
        Add = 0x0004,
        Subtract = 0x0005,
        BeginProcedure = 0x0006,
        EndProcedure = 0x0007,
        BeginArgument = 0x0008,
        EndArgument = 0x0009,
        NumberBase = 0x0200,
        ProcedureNameBase = 0x0400,
        ParameterNameBase = 0x0800
    }
}
